package com.atguigu.exam.service.impl;

import com.atguigu.exam.common.CacheConstants;
import com.atguigu.exam.entity.PaperQuestion;
import com.atguigu.exam.entity.Question;
import com.atguigu.exam.entity.QuestionAnswer;
import com.atguigu.exam.entity.QuestionChoice;
import com.atguigu.exam.mapper.PaperQuestionMapper;
import com.atguigu.exam.mapper.QuestionAnswerMapper;
import com.atguigu.exam.mapper.QuestionChoiceMapper;
import com.atguigu.exam.mapper.QuestionMapper;
import com.atguigu.exam.service.QuestionService;
import com.atguigu.exam.utils.ExcelUtil;
import com.atguigu.exam.utils.RedisUtils;
import com.atguigu.exam.vo.QuestionImportVo;
import com.atguigu.exam.vo.QuestionPageVo;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.injector.methods.DeleteById;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import kotlin.jvm.internal.Lambda;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 题目Service实现类
 * 实现题目相关的业务逻辑
 */
@Slf4j
@Service
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements QuestionService {

    @Autowired
    private QuestionMapper questionMapper;
    @Autowired
    private QuestionChoiceMapper questionChoiceMapper;
    @Autowired
    private QuestionAnswerMapper questionAnswerMapper;
    //@Autowired
    private RedisUtils redisUtils;
    @Autowired
    private PaperQuestionMapper paperQuestionMapper;

    /**
     * 分页查询题目列表（支持多条件筛选）方法一，分步查询
     * @param pageBean       分页查询需要的页数和每页数据的条数
     * @param questionPageVo 封装了题目需要的四个参数，包括类别ID，题型，难度以及关键字
     */
    @Override
    public void customPageService(Page<Question> pageBean, QuestionPageVo questionPageVo) {
        //方法一，分步查询，查询动作在mapper和mapper.xml文件中都已经实现了，这里serviceImpl的作用就是直接调用即可
        questionMapper.customPage(pageBean, questionPageVo);
    }


    /**
     * Java代码进行数据拼装！ 1+n -> 1+1+1  1
     * @param pageBean
     * @param questionPageVo
     */
    @Override
    public void customPageJavaService(Page<Question> pageBean, QuestionPageVo questionPageVo) {
        //方法二，通过Java代码进行优化，实现了从分步查询的1+n+n的复杂度到1+1+1的复杂度的跳跃
        //1、分页查询，多条件，因为这几个条件都是可选的，所以我们需要判断是否为空
        // 使用ObjectUtils.isEmpty()方法，然后前面置非即可
        LambdaQueryWrapper<Question> questionLambdaQueryWrapper = new LambdaQueryWrapper<>();
        //questionPageVo.getType() != null && questionPageVo.getType() != null,因为这个太长
        //我们引入工具类来帮助我们进行判断，ObjectUtils.isEmpty(questionPageVo.getType())和上面等价
        //判断题型【可选】
        questionLambdaQueryWrapper
                .eq(!ObjectUtils.isEmpty(questionPageVo.getType()), Question::getType, questionPageVo.getType());
        //判断类别【可选】
        questionLambdaQueryWrapper
                .eq(!ObjectUtils.isEmpty(questionPageVo.getCategoryId()), Question::getCategoryId, questionPageVo.getCategoryId());
        //判断难度【可选】
        questionLambdaQueryWrapper
                .eq(!ObjectUtils.isEmpty(questionPageVo.getDifficult()), Question::getDifficulty, questionPageVo.getDifficult());
        //判断关键字，模糊匹配，使用like
        questionLambdaQueryWrapper
                .like(!ObjectUtils.isEmpty(questionPageVo.getKeyword()), Question::getTitle, questionPageVo.getKeyword());
        //对题目展示进行排序。创建时间越大，越在上面展示
        questionLambdaQueryWrapper.orderByDesc(Question::getCreateTime);
        //mybatis-plus自带的分页操作，根据拼接的条件和pageBean进行分页
        page(pageBean, questionLambdaQueryWrapper);
        //2、提取一个方法， 给题目进行选项和答案装填
        fillQuestionChoiceAndAnswer(pageBean.getRecords());
    }

    /**
     * 根据id查询题目详情
     * @param id 题目id
     * @return 返回题目详情对象
     */
    @Override
    public Question customDetailQuestion(Long id) {
        //1、查询题目详情
        Question question = questionMapper.customGetById(id);
        //对查询结果进行判断，如果为空，抛出异常
        if (question == null) {
            throw new RuntimeException("题目查询详情失败！原因可能提前被删除！题目id为：" + id);
        }
        //2、进行热点题目缓存
        //这里是根据点击次数进行判断，创建一个新的子线程，进行题目数量的计数
        new Thread(() -> {
            incrementQuestion(question.getId());
        }).start();
        return question;
    }

    private void incrementQuestion(Long questionId) {
        Double score = redisUtils.zIncrementScore(CacheConstants.POPULAR_QUESTIONS_KEY, questionId, 1);
        log.info("完成{}题目分数累计，累计后分数为：{}", questionId, score);
    }

    /***
     * 提取出来对题目进行选项和答案的填充
     * 这里提取出来这个方法的原因是为了进行代码复用，在热题查询，分页查询题目等模块都涉及查询题目集合，都需要进行填充
     * @param questionList
     */
    private void fillQuestionChoiceAndAnswer(List<Question> questionList) {
        //1、先对传入的参数进行非空判断，如果为空，就直接完事了，不需要执行
        //这里在判断空的时候，如果为空，不像之前那样抛出异常，是因为这是查询，当一打开页面的时候，就相当于查询
        //这个查询不需要提示错误信息，没有就不展示即可，所以这里就不抛出异常，而是直接通过 return 结束方法
        if (questionList == null || questionList.isEmpty()) {
            log.debug("没有查询对应的问题集合数据！！");
            return;
        }
        //2. 查询所有答案和选项
        //思路：最开始，我们可以直接查询所有的答案和选项，首先我们把对应的mapper注入
        //List<QuestionChoice> questionChoices = questionChoiceMapper.selectList(null);
        //List<QuestionAnswer> questionAnswers = questionAnswerMapper.selectList(null);

        //之后，我们发现，可以优化性能，只需要查询本地题目的选项和答案即可，不需要全部查询
        //查询本次，就是获取本次的题目ID即可，然后根据ID查询即可
        List<Long> collect = questionList.stream().map(Question::getId).collect(Collectors.toList());
        //我们查询本次题目，也就是说只要ID在这个collect集合中即可，
        List<QuestionChoice> questionChoices =
                questionChoiceMapper.selectList(new LambdaQueryWrapper<QuestionChoice>().in(QuestionChoice::getQuestionId, collect));
        List<QuestionAnswer> questionAnswers =
                questionAnswerMapper.selectList(new LambdaQueryWrapper<QuestionAnswer>().in(QuestionAnswer::getQuestionId, collect));

        //3、答案和选项进行map转化，
        //对于选项而言，因为一个题目可能对应多个选项，所以我们直接分组，然后转化成map，key是问题ID，然后value是对应的选项集合
        Map<Long, List<QuestionChoice>> choicemap =
                questionChoices.stream().collect(Collectors.groupingBy(QuestionChoice::getQuestionId));
        //对于答案而言，一个选项只有一个答案,所以直接查找就可以
        //注意：直接questionAnswers.stream().map()表示将里面的每一个都转成map,而collect(Collectors.tomap是转换成一个大map)
        //这里的a->a代表自己整个，整个作为一个默认值
        Map<Long, QuestionAnswer> answermap =
                questionAnswers.stream().collect(Collectors.toMap(QuestionAnswer::getQuestionId, a -> a));

        //4、循环问题集合，进行选项和答案配置
        questionList.forEach(question -> {
            //每个题目一定有答案，所以所有题目的答案属性一定会被赋值,根据ID查询答案map，然后value就是答案，赋值进去
            question.setAnswer(answermap.get(question.getId()));

            //只有选择题才会有选项，所以只有当type是选择题的时候，才需要对选项属性进行赋值
            //我们需要进行判断，只要type属性是Choices，说明是选择题
            if ("CHOICE".equals(question.getType())) {
                //因为选项也需要考虑排序，所以我们把获取的选项提取出来，先进行排序，然后直接进行赋值
                List<QuestionChoice> questionChoices1 = choicemap.get(question.getId());
                //比较器和其优化
                questionChoices1.sort(Comparator.comparingInt(QuestionChoice::getSort));
                //填充选项属性
                question.setChoices(questionChoices1);
            }
        });
    }

    /**
     * 添加新的考试题目，支持选择题、判断题、简答题等多种题型
     * @param question 传入要保存的问题
     *                 分析：一共涉及到三张表，题目表，选项表，答案表，只有选择题的时候会涉及到选项表，但是每个题目一定有答案
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void customSaveQuestion(Question question) {
        //1.一定插入题目信息 （回显题目id）
        //同一个类型不能题目title相同
        LambdaQueryWrapper<Question> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Question::getType, question.getType());
        queryWrapper.eq(Question::getTitle, question.getTitle());
        //自己的业务或者自己的mapper: getBaseMapper() baseMapper
        boolean exists = baseMapper.exists(queryWrapper);
        if (exists) {
            //同一类型，title相同
            throw new RuntimeException("在%s下，存在%s 名称的题目已经存在！保存失败！".formatted(question.getType(), question.getTitle()));
        }

        boolean saved = save(question);
        if (!saved) {
            //同一类型，title相同
            throw new RuntimeException("在%s下，存在%s 名称的题目！保存失败！".formatted(question.getType(), question.getTitle()));
        }
        //2.获取答案对象，并先配置题目id
        QuestionAnswer answer = question.getAnswer();
        answer.setQuestionId(question.getId());
        //3.判断是不是选择题
        if ("CHOICE".equals(question.getType())) {
            //是 -》 循环 -》 选项 + 题目id -> 保存 -》 判断是不是正确 进行 AD
            List<QuestionChoice> choices = question.getChoices();
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < choices.size(); i++) {
                //给每个选项匹配questionId
                // [0 [1] 2 [3] ]
                QuestionChoice choice = choices.get(i);
                //确保选项的顺序，保证每次查询都不变
                choice.setSort(i);

                choice.setQuestionId(question.getId());
                questionChoiceMapper.insert(choice);
                if (choice.getIsCorrect()) {
                    //true 本次是正确答案
                    if (sb.length() > 0) {
                        sb.append(",");
                    }
                    //B,D
                    sb.append((char) ('A' + i));
                }
            }

            //进行答案赋值
            answer.setAnswer(sb.toString());
        }
        // 4.保存答案对象
        questionAnswerMapper.insert(answer);
        // 5.保证方法的一致性！ 需要添加事务
    }

    /**
     * 修改指定题目的内容、选项、答案等信息
     * @param question
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void customUpdateQuestion(Question question) {
        //1、对题目进行校验，id不同，title不能相同
        LambdaQueryWrapper<Question> questionLambdaQueryWrapper = new LambdaQueryWrapper<>();
        questionLambdaQueryWrapper.eq(Question::getTitle, question.getTitle());
        questionLambdaQueryWrapper.ne(Question::getId, question.getId());
        boolean exists = baseMapper.exists(questionLambdaQueryWrapper);
        if (exists) {
            throw new RuntimeException("修改：%s题目的新标题：%s和其他的题目重复了！修改失败！".formatted(question.getId(), question.getTitle()));
        }
        //2、修改题目,需要进行校验
        boolean update = updateById(question);
        if (!update) {
            throw new RuntimeException("修改：%s题目失败！！".formatted(question.getId()));
        }
        //3、获取答案对象
        QuestionAnswer answer = question.getAnswer();
        //4、判断是不是选择题
        if ("CHOICE".equals(question.getType())) {
            //是选择题，需要删除选项，然后重新添加题目
            //删除题目对应的所有选项(原)
            LambdaQueryWrapper<QuestionChoice> questionChoiceLambdaQueryWrapper = new LambdaQueryWrapper<>();
            questionChoiceLambdaQueryWrapper.eq(QuestionChoice::getQuestionId, question.getId());
            questionChoiceMapper.delete(questionChoiceLambdaQueryWrapper);
            //循环新增题目，将id赋为null
            //
            StringBuilder stringBuilder = new StringBuilder();
            List<QuestionChoice> choices = question.getChoices();
            for (int i = 0; i < choices.size(); i++) {
                QuestionChoice questionChoice = choices.get(i);
                //确保选项的顺序，保证每次查询都不变
                questionChoice.setSort(i);
                //置空，重置选项对象的元数据字段，为重新插入数据库做准备
                questionChoice.setId(null);
                questionChoice.setCreateTime(null);
                questionChoice.setUpdateTime(null);
                //如果当面对新增选项的时候，我们发现，无法添加成功，这是因为选项

                //这里需要考虑到之后如果对于考试题目判断的话，只有结果为true的题目的答案才会被进行修改，也就是这样的追加，
                //如果结果为false，答案不需要根据选项去修改
                if (questionChoice.getIsCorrect()) {
                    //选择题答案的拼接
                    if (stringBuilder.length() > 0) {
                        stringBuilder.append(",");
                    }
                    stringBuilder.append((char) ('A' + 1));
                }
                //选择题答案赋值
                answer.setAnswer(stringBuilder.toString());
            }
        }
        //5、进行答案修改
        questionAnswerMapper.updateById(answer);
        //6、保证一致性，添加事务
    }

    /**
     * 根据ID删除指定的题目，包括关联的选项和答案数据
     * @param id 分析：我们观察所有表，和题目有关的表一共有5张，分类表，考试题目表，选项表，答案表，试卷记录表
     *           这里面，分类表和试卷记录表， 题目的删除对他们没有任何影响
     *           选项表和答案表，属于题目表的子表，随着题目表的删除而删除
     *           考试题目表，根据业务场景来描述，如果和这张表存在引用，那么不允许删除
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void customRemoveQuestionById(Long id) {
        //1、判断试卷题目表，存在删除失败的可能,如果在试卷题目信息表中存在对应题目的引用，即存在对应的id,那么就无法删除
        LambdaQueryWrapper<PaperQuestion> paperQuestionLambdaQueryWrapper = new LambdaQueryWrapper<>();
        paperQuestionLambdaQueryWrapper.eq(PaperQuestion::getQuestionId, id);
        Long count = paperQuestionMapper.selectCount(paperQuestionLambdaQueryWrapper);
        //count > 0表示存在引用，那么就需要抛出异常，无法删除
        if (count > 0) {
            throw new RuntimeException("该题目：%s 被试卷表中引用%s次，删除失败！".formatted(id, count));
        }
        //2、删除主表题目表,对于这种数据的修改操作，需要进行数据校验
        boolean remove = removeById(id);
        if (!remove) {
            throw new RuntimeException("该题目：%s 信息删除失败！！");
        }
        //3、删除子表 答案和选项表
        questionChoiceMapper.delete(new LambdaQueryWrapper<QuestionChoice>().eq(QuestionChoice::getQuestionId, id));
        questionAnswerMapper.delete(new LambdaQueryWrapper<QuestionAnswer>().eq(QuestionAnswer::getQuestionId, id));
        //为了保证一致性，添加事务
    }

    /**
     * 预览Excel文件内容（不入库）
     * @param file
     * @return
     */
    @Override
    public List<QuestionImportVo> preViewExcel(MultipartFile file) throws IOException {
        //1、参数校验，文件不能为NULL 同时文件后缀名要是xls或者xlsx
        if (file == null || file.isEmpty()) {
            throw new RuntimeException("预览数据的文件为空！");
        }
        String filename = file.getOriginalFilename();
        //表示文件后缀名不为xls或者xlsx，抛出异常
        if (!filename.endsWith(".xls") && !filename.endsWith(".xlsx")) {
            throw new RuntimeException("预览数据的文件格式错误，必须是 .xls或者.xlsx！");
        }
        //调用解析方法
        List<QuestionImportVo> questionImportVos = ExcelUtil.parseExcel(file);
        //返回结果
        return questionImportVos;
    }

    /**
     * 批量导入题目（通用接口，支持Excel导入或AI生成后的确认导入）
     * @param questions 题目导入DTO列表
     * @return 返回导入成功的题目数量
     */
    @Override
    public int importBatchQuestions(List<QuestionImportVo> questions) {
        //1、对数据进行校验，不可为空，
        if (questions == null || questions.isEmpty()) {
            throw new RuntimeException("导入的题目集合为空！");
        }
        //2、将给的vo数据转换为question对象，为了复用之前的单条导入方法
        // 这里需要使用for循环+try catch来保证即使中间有数据导入失败，不影响其他数据的导入
        //这个值是统计成功的条数
        int count = 0;
        //一般遇到批量操作，同时忽略中间失败的场景，需要考虑这种结构，官方称呼，鲁棒原则
        for (int i = 0; i < questions.size(); i++) {
            try {
                //2. 进行vo - question [提取一个方法]
                Question question = convertQuestionImportVoToQuestion(questions.get(i));
                //调用之前写的单条的题目的保存方法
                customSaveQuestion(question);
                //成功条数+1
                count++;
            } catch (Exception e) {
                //失败提示
                log.debug("{}题目导入失败！", questions.get(i).getTitle());
            }
        }
        //3、获得成功的条数，进行返回即可
        return count;
    }

    /**
     * 未完成
     * 通过Excel进行题目批量导入
     * @param file
     * @return
     */
    @Override
    public String importExeclBatchQuestions(MultipartFile file) throws IOException {
        //1.校验
        if (file == null || file.isEmpty()){
            throw new RuntimeException("批量导入的文件为空！");
        }
        String filename = file.getOriginalFilename();
        if (!filename.endsWith(".xls") && !filename.endsWith(".xlsx")){
            throw new RuntimeException("批量导入的文件格式错误，必须是xls或者xlsx格式！");
        }
        //2.解析文件
        List<QuestionImportVo> questionImportVoList = ExcelUtil.parseExcel(file);
        //3.调用批量导入业务
        int successCount = importBatchQuestions(questionImportVoList);
        String result = "批量导入题目接口调用成功！ 一共：%s 题目需要导入，成功导入了：%s 道题！".formatted(questionImportVoList.size(),successCount);
        return result;
    }

    /**
     * 将一个questionVo对象转换成一个question对象，方便之后复用方法
     * @param questionImportVo
     * @return
     */
    private Question convertQuestionImportVoToQuestion(QuestionImportVo questionImportVo) {
        //1、给question本体赋值，先创建一个question
        Question question = new Question();

        //2、转换，因为question里面和QuestionImportVo的很多内容都相同，且属性名都对应相同
        // 所以我们可以直接使用 BeanUtils.copyProperties方法，赋值相同属性即可
        BeanUtils.copyProperties(questionImportVo,question);

        //3、针对选择题的答案部分，因为QuestionImportVo对于选择题，没有答案
        // 我们在其他地方进行了答案的处理，但是有个前提，需要question先有一个答案的对象
        // 也就是需要显式地将答案对象赋值为NULL，之后就不会出现空指针问题

        //判断是不是选择题,选择题，给选项集合进行赋值
        if("CHOICE".equals(questionImportVo.getType())){
            //判断选项是不是为空，即只传了题目，没有传选项
            if(questionImportVo.getChoices().size()>0){
                ArrayList<Question> questionChoices = new ArrayList<>(questionImportVo.getChoices().size());
            }
        }
        //4、针对判断题，我们在测试的时候发现，他会将传递的大写True和False自动转为小写，但是前端不识别小写，所以我们需要将答案转为大写

        return null;
    }


}